package vip.xiaonuo.biz.modular.timetable.domain;

import cn.hutool.core.util.ObjectUtil;
import vip.xiaonuo.biz.modular.timetable.algorithm.Individual;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Timetable is the main evaluation class for the class scheduler GA.
 * 
 * A timetable represents a potential solution in human-readable form, unlike an
 * Individual or a chromosome. This timetable class, then, can read a chromosome
 * and develop a timetable from it, and ultimately can evaluate the timetable
 * for its fitness and number of scheduling clashes.
 * 
 * The most important methods in this class are createClasses and calcClashes.
 * 
 * The createClasses method accepts an Individual (really, a chromosome),
 * unpacks its chromosome, and creates Class objects from the genetic
 * information. Class objects are lightweight; they're just containers for
 * information with getters and setters, but it's more convenient to work with
 * them than with the chromosome directly.
 * 
 * The calcClashes method is used by GeneticAlgorithm.calcFitness, and requires
 * that createClasses has been run first. calcClashes looks at the Class objects
 * created by createClasses, and figures out how many hard constraints have been
 * violated.
 * 
 */
public class Timetable {
	public static final String DUMMY_ID = "0000000000000000000";
	private final HashMap<String, Room> rooms;
	private final HashMap<String, Professor> professors;
	private final HashMap<String, Module> modules;
	private final HashMap<String, Klass> klasses;
	private final HashMap<String, Lecture> lectures;
	private final HashMap<String, Timeslot> timeslots;
	private final List<Lesson> scheduledLessons;
	private Lesson lessons[];

	private int numLessons = 0;

	/**
	 * Initialize new Timetable
	 */
	public Timetable() {
		this.rooms = new HashMap<String, Room>();
		this.professors = new HashMap<String, Professor>();
		this.modules = new HashMap<String, Module>();
		this.klasses = new HashMap<String, Klass>();
		this.lectures = new HashMap<String, Lecture>();
		this.timeslots = new HashMap<String, Timeslot>();
		this.scheduledLessons = new ArrayList<>();
		this.lessons = new Lesson[0];
	}

	/**
	 * "Clone" a timetable. We use this before evaluating a timetable so we have
	 * a unique container for each set of classes created by "createClasses".
	 * Truthfully, that's not entirely necessary (no big deal if we wipe out and
	 * reuse the .classes property here), but Chapter 6 discusses
	 * multi-threading for fitness calculations, and in order to do that we need
	 * separate objects so that one thread doesn't step on another thread's
	 * toes. So this constructor isn't _entirely_ necessary for Chapter 5, but
	 * you'll see it in action in Chapter 6.
	 * 
	 * @param cloneable
	 */
	public Timetable(Timetable cloneable) {
		this.rooms = cloneable.getRooms();
		this.professors = cloneable.getProfessors();
		this.modules = cloneable.getModules();
		this.klasses = cloneable.getKlasses();
		this.lectures = cloneable.getLectures();
		this.timeslots = cloneable.getTimeslots();
		this.scheduledLessons = cloneable.getScheduledLessons();
		this.lessons = cloneable.getLessons();
	}

	private HashMap<String, Klass> getKlasses() {
		return this.klasses;
	}
	private HashMap<String, Lecture> getLectures() {
		return this.lectures;
	}

	private HashMap<String, Timeslot> getTimeslots() {
		return this.timeslots;
	}

	private HashMap<String, Module> getModules() {
		return this.modules;
	}

	private HashMap<String, Professor> getProfessors() {
		return this.professors;
	}

	private List<Lesson> getScheduledLessons() {
		return this.scheduledLessons;
	}

	/**
	 * Add new room
	 * 
	 * @param roomId
	 * @param roomName
	 * @param capacity
	 * @param roomType
	 */
	public void addRoom(String roomId, String roomName, int capacity, String roomType) {
		this.rooms.put(roomId, new Room(roomId, roomName, capacity, roomType));
	}

	/**
	 * Add new professor
	 * 
	 * @param professorId
	 * @param professorName
	 */
	public void addProfessor(String professorId, String professorName) {
		this.professors.put(professorId, new Professor(professorId, professorName));
	}

	/**
	 * Add new module
	 *
	 * @param moduleId
	 * @param module
	 * @param timesPerWeek
	 * @param professorIds
	 */
	public void addModule(String moduleId, String module, int timesPerWeek, String[] professorIds) {
		this.modules.put(moduleId, new Module(moduleId, module, timesPerWeek, professorIds));
	}

	public void addKlass(String klassId, String klassName) {
		this.klasses.put(klassId, new Klass(klassId, klassName));
	}
	/**
	 * Add new lecture
	 *
	 * @param lectureId
	 * @param lectureState
	 * @param lectureSize
	 * @param fromWeek
	 * @param toWeek
	 * @param roomType
	 * @param maxClassHour
	 * @param klassIds
	 * @param moduleIds
	 */
	public void addLecture(String lectureId, String lectureState, int lectureSize, int fromWeek, int toWeek, String roomType, int maxClassHour, String[] klassIds, String[] moduleIds) {
		this.lectures.put(lectureId, new Lecture(lectureId, lectureState, lectureSize, fromWeek, toWeek, roomType, maxClassHour, klassIds, moduleIds));
		this.numLessons = 0;
	}

	/**
	 * Add new timeslot
	 *
	 * @param timeslotId
	 * @param timeslot
	 * @param code
	 */
	public void addTimeslot(String timeslotId, String timeslot, String code) {
		this.timeslots.put(timeslotId, new Timeslot(timeslotId, timeslot, code));
	}

	public void addScheduledLesson(String lessonId, String lectureId, String moduleId, String professorId, String timeslotId, String roomId, String[] klassIds) {
		this.scheduledLessons.add(new Lesson(lessonId, lectureId, moduleId, professorId, timeslotId, roomId, klassIds));
	}
	/**
	 * Create classes using individual's chromosome
	 * 
	 * One of the two important methods in this class; given a chromosome,
	 * unpack it and turn it into an array of Class (with a capital C) objects.
	 * These Class objects will later be evaluated by the calcClashes method,
	 * which will loop through the Classes and calculate the number of
	 * conflicting timeslots, rooms, professors, etc.
	 * 
	 * While this method is important, it's not really difficult or confusing.
	 * Just loop through the chromosome and create Class objects and store them.
	 * 
	 * @param individual
	 */
	public void createLessons(Individual individual) {
		// Init lessons
		Lesson lessons[] = new Lesson[this.getNumLessons()];

		// Get individual's chromosome
		String chromosome[] = individual.getChromosome();
		int chromosomePos = 0;
		int lessonIndex = 0;

		for (Lecture lecture : this.getSchedulingLecturesAsArray()) {
			for (String moduleId : lecture.getModuleIds()) {
				for (int i = 0; i < this.getModule(moduleId).getTimesPerWeek(); i++) {
					lessons[lessonIndex] = new Lesson(String.valueOf(lessonIndex), lecture.getLectureId(), moduleId, lecture.getKlassIds());

					// Add timeslot
					lessons[lessonIndex].addTimeslot(String.valueOf(chromosome[chromosomePos]));
					chromosomePos++;

					// Add room
					lessons[lessonIndex].setRoomId(String.valueOf(chromosome[chromosomePos]));
					chromosomePos++;

					// Add professor
					lessons[lessonIndex].addProfessor(String.valueOf(chromosome[chromosomePos]));
					chromosomePos++;

					lessonIndex++;
				}
			}
		}

		this.lessons = lessons;
	}

	/**
	 * Get room from roomId
	 * 
	 * @param roomId
	 * @return room
	 */
	public Room getRoom(String roomId) {
		if (!this.rooms.containsKey(roomId)) {
			System.out.println("Rooms doesn't contain key " + roomId);
		}
		return (Room) this.rooms.get(roomId);
	}

	public HashMap<String, Room> getRooms() {
		return this.rooms;
	}

	/**
	 * Get random room
	 * 
	 * @return room
	 */
	public Room getRandomRoom() {
		Object[] roomsArray = this.rooms.values().stream().filter(x -> !DUMMY_ID.equalsIgnoreCase(x.getRoomId())).toArray();
		Room room = (Room) roomsArray[(int) (roomsArray.length * Math.random())];
		return room;
	}

	/**
	 * Get professor from professorId
	 * 
	 * @param professorId
	 * @return professor
	 */
	public Professor getProfessor(String professorId) {
		return (Professor) this.professors.get(professorId);
	}

	/**
	 * Get module from moduleId
	 * 
	 * @param moduleId
	 * @return module
	 */
	public Module getModule(String moduleId) {
		return (Module) this.modules.get(moduleId);
	}

	/**
	 * Get lecture from lectureId
	 * 
	 * @param lectureId
	 * @return lecture
	 */
	public Lecture getLecture(String lectureId) {
		return (Lecture) this.lectures.get(lectureId);
	}

	public Lecture[] getSchedulingLecturesAsArray() {
		Collection<Lecture> lectures = this.lectures.values();
		List<Lecture> schedulingLectures = lectures.stream().filter(x -> !"1".equalsIgnoreCase(x.getLectureState())).collect(Collectors.toList());
		return (Lecture[]) schedulingLectures.toArray(new Lecture[schedulingLectures.size()]);
	}

	/**
	 * Get timeslot by timeslotId
	 * 
	 * @param timeslotId
	 * @return timeslot
	 */
	public Timeslot getTimeslot(String timeslotId) {
		return (Timeslot) this.timeslots.get(timeslotId);
	}

	/**
	 * Get random timeslotId
	 * 
	 * @return timeslot
	 */
	public Timeslot getRandomTimeslot() {
		Object[] timeslotArray = this.timeslots.values().toArray();
		Timeslot timeslot = (Timeslot) timeslotArray[(int) (timeslotArray.length * Math.random())];
		return timeslot;
	}

	/**
	 * Get lessons
	 * 
	 * @return lessons
	 */
	public Lesson[] getLessons() {
		return this.lessons;
	}

	/**
	 * Get number of lessons that need scheduling
	 * 
	 * @return numClasses
	 */
	public int getNumLessons() {
		if (this.numLessons > 0) {
			return this.numLessons;
		}

		int numLessons = 0;
		Lecture lectures[] = getSchedulingLecturesAsArray();
		for (Lecture lecture : lectures) {
			for (String moduleId : lecture.getModuleIds()) {
				Module module = this.getModule(moduleId);
				numLessons+= module.getTimesPerWeek();
			}
		}
		this.numLessons = numLessons;

		return this.numLessons;
	}

	/**
	 * Calculate the number of clashes between Classes generated by a
	 * chromosome.
	 * 
	 * The most important method in this class; look at a candidate timetable
	 * and figure out how many constraints are violated.
	 * 
	 * Running this method requires that createClasses has been run first (in
	 * order to populate this.classes). The return value of this method is
	 * simply the number of constraint violations (conflicting professors,
	 * timeslots, or rooms), and that return value is used by the
	 * GeneticAlgorithm.calcFitness method.
	 * 
	 * There's nothing too difficult here either -- loop through this.classes,
	 * and check constraints against the rest of the this.classes.
	 * 
	 * The two inner `for` loops can be combined here as an optimization, but
	 * kept separate for clarity. For small values of this.classes.length it
	 * doesn't make a difference, but for larger values it certainly does.
	 * 
	 * @return numClashes
	 */
	public int calcClashes() {
		List<Lesson> allLessons = new ArrayList<>();
		allLessons.addAll(this.scheduledLessons);
		allLessons.addAll(Arrays.asList(this.lessons));
		int clashes = 0;

		for (Lesson lessonA : allLessons) {
			// Check room
			if(!isRoomQualified(lessonA)) {
				clashes++;
			}

			for (Lesson lessonB : allLessons) {
				// At same time, different lessons
				if (isDifferentLesson(lessonA, lessonB) && isSameTime(lessonA, lessonB)) {
					// Check if room is available
					if (isSameRoom(lessonA, lessonB)) {
						clashes++;
						break;
					}
					// Check if professor is available
					if (isSameProfessor(lessonA, lessonB)) {
						clashes++;
						break;
					}
					// check if klass group is available
					if (isSameLecture(lessonA, lessonB)) {
						clashes++;
						break;
					}
					// check if each klass is available
					if (haveCommonKlass(lessonA, lessonB)) {
						clashes++;
						break;
					}
				}
			}
		}

		return clashes;
	}

	public int calcScores() {
		List<Lesson> allLessons = new ArrayList<>();
		allLessons.addAll(this.scheduledLessons);
		allLessons.addAll(Arrays.asList(this.lessons));
		int scores = 0;
		for (Lesson lessonA : allLessons) {
			for (Lesson lessonB : allLessons) {
				if (isSameLecture(lessonA, lessonB) && isSameRoom(lessonA, lessonB)) {
					scores += 10;
					if (this.getLecture(lessonA.getLectureId()).getMaxClassHour() == 4 && isSequential(lessonA, lessonB)) {
						scores += 5;
					}
				}
			}
		}
		return scores;
	}

	private boolean isRoomQualified(Lesson lesson) {
		if (DUMMY_ID.equalsIgnoreCase(lesson.getRoomId())) {
			return true;
		}
		Room room = this.getRoom(lesson.getRoomId());
		Lecture lecture = this.getLecture(lesson.getLectureId());

		if (room.getRoomCapacity() < lecture.getLectureSize()) {
			return false;
		}
		if (!room.getRoomType().equalsIgnoreCase(lecture.getRoomType())) {
			return false;
		}
		return true;
	}

	private boolean isDifferentLesson(Lesson lessonA, Lesson lessonB) {
		return ObjectUtil.notEqual(lessonA.getLessonId(), lessonB.getLessonId());
	}
	private boolean isSameLecture(Lesson lessonA, Lesson lessonB) {
		return ObjectUtil.equals(lessonA.getLectureId(), lessonB.getLectureId());
	}

	private boolean isSameRoom(Lesson lessonA, Lesson lessonB) {
		return isSpecificRoom(lessonA) && isSpecificRoom(lessonB) && ObjectUtil.equals(lessonA.getRoomId(), lessonB.getRoomId());
	}
	private boolean isSameTime(Lesson lessonA, Lesson lessonB) {
		return ObjectUtil.equals(lessonA.getTimeslotId(), lessonB.getTimeslotId()) && haveOverlappingWeek(lessonA, lessonB);
	}
	private boolean isSameProfessor(Lesson lessonA, Lesson lessonB) {
		return isSpecificProfessor(lessonA) && isSpecificProfessor(lessonB) && ObjectUtil.equals(lessonA.getProfessorId(), lessonB.getProfessorId());
	}
	private boolean isSpecificRoom(Lesson lesson) {
		return !DUMMY_ID.equalsIgnoreCase(lesson.getRoomId());
	}
	private boolean isSpecificProfessor(Lesson lesson) {
		return !DUMMY_ID.equalsIgnoreCase(lesson.getProfessorId());
	}
	private boolean haveOverlappingWeek(Lesson lessonA, Lesson lessonB) {
		Lecture a = getLecture(lessonA.getLectureId());
		Lecture b = getLecture(lessonB.getLectureId());
		return (a.getFromWeek() <= b.getFromWeek() && b.getFromWeek() <= a.getToWeek()) ||
				(b.getFromWeek() <= a.getFromWeek() && a.getFromWeek() <= b.getToWeek());
	}
	private boolean isSequential(Lesson lessonA, Lesson lessonB) {
		int ta = Integer.valueOf(this.getTimeslot(lessonA.getTimeslotId()).getCode());
		int tb = Integer.valueOf(this.getTimeslot(lessonB.getTimeslotId()).getCode());
		if (ta/10 != tb/10) return false;
		if (Math.abs(ta - tb) != 2) return false;
		if (ta < tb) {
			return ((ta % 10) == 1 || (ta % 10) == 5) ? true : false;
		} else {
			return ((ta % 10) == 3 || (ta % 10) == 7) ? true : false;
		}
	}

	private boolean haveCommonKlass(Lesson lessonA, Lesson lessonB) {
		for (String klassId: lessonA.getKlassIds()) {
			for (String anotherKlassId: lessonB.getKlassIds()) {
				if (klassId.equalsIgnoreCase(anotherKlassId)) {
					return true;
				}
			}
		}
		return false;
	}
}